Day 16 最後,我們先把登入按鈕寫成這樣:
<button
onClick={() => {
setUser({
name: 'Anthony'
})
}}
>
Sign In
</button>
當時我先把它理解成:
按下 Sign In
↓
執行 setUser(...)
↓
user 更新
↓
React 重新 render
這樣用沒有問題。
onClick={() => { ... }}
為什麼中間要多一個 () => {}?
不過我們要的不就是「按下去時執行 setUser()」嗎?
那直接這樣不行嗎?
onClick={setUser({
name: 'Anthony'
})}
先看三個長得很像的東西:
login
login()
() => login()
它們其實不是同一件事。
login
→ login 這個 function 本身
login()
→ 現在立刻呼叫 login
() => login()
→ 建立一個新的 function
→ 這個新 function 之後被呼叫時,才執行 login()
而 React 的 onClick 要的,正是:
一個可以等到 click 發生時再被呼叫的 function。
所以今天真正要看懂的,不是 Arrow Function 長什麼樣。
而是這兩個括號:
login
和:
login()
到底差在哪裡。
setUser,自己寫一個最簡單的 function先寫:
function login() {
console.log('登入')
}
我希望做到:
按 Sign In
↓
執行 login
↓
Console 出現「登入」
React 可以這樣寫:
<button onClick={login}>
Sign In
</button>
第一次看到這裡其實很怪。
因為平常我要執行 function,不是都寫:
login()
嗎?
為什麼這裡反而少了 ()?
答案是:因為現在還不要執行。
這時候我只是先把 login 這個 function 交給 React。
可以先想成:
React render
↓
拿到 login 這個 function
↓
先不執行
↓
使用者真的 click
↓
React 呼叫 login
所以:
onClick={login}
不是少寫了括號。
而是故意不寫括號。
這一篇很適合直接看 Console,不然很容易只是把答案背起來。
先寫:
function App() {
console.log('1. App render')
function login() {
console.log('2. login 執行')
}
return (
<button onClick={login}>
Sign In
</button>
)
}
你會先看到:
1. App render
這時候還不應該因為 onClick={login} 就看到:
2. login 執行
等真的按下 Sign In,login 才會執行。
這就是:
onClick={login}
最重要的行為。
它不是「render 時執行 login」。
而是「render 時把 login 當成事件處理 function 交給 React」。
onClick={login()} 呢?把剛剛那行改成:
<button onClick={login()}>
Sign In
</button>
這次事情完全不一樣。
因為 JSX 在 render 時,JavaScript 必須先算出大括號裡這個 expression 的結果:
login()
而看到 (),意思就是:
現在呼叫 login
所以流程會變成:
React render
↓
計算 login()
↓
login 立刻執行
↓
login 的回傳值才會成為 onClick 收到的值
也就是說,我根本還沒按按鈕,login() 就已經跑了。
如果 login() 沒有特別 return 一個 function,那它通常回傳的是 undefined。
所以這個:
onClick={login()}
其實比較像:
先把 login 執行掉
↓
再把 login() 的結果交給 onClick
這當然不是我們原本要的「等 click 再登入」。
setUser(),問題就很清楚了Day 16 的:
setUser({
name: 'Anthony'
})
本身就是一次 function call。
所以如果寫:
<button
onClick={setUser({
name: 'Anthony'
})}
>
Sign In
</button>
JavaScript 在 render 這個 Component 時,就會先執行:
setUser({
name: 'Anthony'
})
但 setUser 又會要求 React 更新 state。
於是很可能變成:
render
↓
執行 setUser
↓
state 更新
↓
又 render
↓
又執行 setUser
↓
...
這也是為什麼這種寫法不只是「時機不太對」。
它甚至可能直接造成重複 render 的錯誤。
我們真正要的是:
現在 render
→ 先準備好登入這件事情
未來 click
→ 才執行 setUser(...)
() => {} 是在幫我們把執行時間往後推現在再看原本那段:
onClick={() => {
setUser({
name: 'Anthony'
})
}}
先不要把它看成一大坨 React 語法。
只看這一段:
() => {
setUser({
name: 'Anthony'
})
}
它本身是在建立一個 function。
建立 function 不等於執行 function。
所以 render 時發生的是:
建立一個新的 function
↓
把這個 function 交給 onClick
↓
先不執行裡面的 setUser
真的 click 之後才會:
React 呼叫這個 function
↓
function 裡面執行 setUser(...)
↓
user 更新
所以 Arrow Function 在這裡最重要的作用,不是「React 規定一定要這樣寫」。
而是:
先建立一個 function,把真正要做的事包在裡面,等 click 時再執行。
login,反而更好懂原本:
<button
onClick={() => {
setUser({
name: 'Anthony'
})
}}
>
Sign In
</button>
可以先拆成:
function login() {
setUser({
name: 'Anthony'
})
}
再寫:
<button onClick={login}>
Sign In
</button>
對我現在來說,這版反而比較容易讀。
因為三件事情被拆開了:
login
→ 登入時要做什麼
onClick
→ 什麼時候做
onClick={login}
→ click 的時候,做 login
這比一開始硬背:
onClick={() => { ... }}
有畫面很多。
假設 login 要接收名字:
function login(name) {
setUser({
name: name
})
}
我們希望 click 之後才執行:
login('Anthony')
但不能寫:
onClick={login('Anthony')}
因為這又會在 render 時直接呼叫 login('Anthony')。
所以可以包一層:
onClick={() => login('Anthony')}
現在 onClick 拿到的是:
() => login('Anthony')
這個新的 function。
等 click 發生,React 才呼叫它;它再去呼叫:
login('Anthony')
所以可以先記成:
onClick={login}
→ 把現成的 login function 交出去
onClick={() => login('Anthony')}
→ 先建立一個 function 交出去
→ click 後才用指定參數呼叫 login
不是「React event 一律要寫 Arrow Function」。
而是要看:你現在交給 onClick 的到底是不是一個 function。
onClick={login}login
→ function 本身
→ 交給 React
→ click 時再呼叫
onClick={login()}login()
→ 現在立刻呼叫
→ 再把回傳值交給 onClick
一般按鈕事件不是我們要的效果。
onClick={() => login('Anthony')}() => login('Anthony')
→ 建立新的 function
→ 交給 React
→ click 時才執行 login('Anthony')
三個看起來只差幾個符號,但「執行的時間點」完全不同。
現在完整的最小會員範例可以寫成:
import { useState } from 'react'
function App() {
const [user, setUser] = useState(null)
function login() {
setUser({
name: 'Anthony'
})
}
return (
<div>
<p>
{user ? `Hello, ${user.name}` : '尚未登入'}
</p>
<button onClick={login}>
Sign In
</button>
</div>
)
}
export default App
現在整條流程可以講成:
第一次 render
↓
user 是 null
↓
畫面顯示「尚未登入」
↓
onClick 拿到 login function
↓
等待使用者操作
↓
使用者 click
↓
React 呼叫 login
↓
login 執行 setUser(...)
↓
user 更新
↓
React 再 render
↓
畫面顯示 Hello, Anthony
Day 16 解決的是:
React 要記住什麼資料?
Day 17 補上的則是:
使用者做了什麼事情,才要去改那份資料?
State 和 Event 到這裡才真的接起來。
我以前看到:
onClick={...}
很容易把它理解成:
大括號裡放「按下去之後要執行的程式碼」。
這句話不夠準。
因為 JSX 在 render 時,本來就會計算大括號裡的 JavaScript expression。
更準確的理解是:
onClick要拿到一個 function;React 等 click 發生時,再呼叫這個 function。
所以:
onClick={login}
成立,因為 login 是 function。
這個也成立:
onClick={() => login()}
因為 () => login() 也是 function。
但:
onClick={login()}
會先執行 login(),再把它的回傳值交給 onClick。
這才是今天那兩個括號真正重要的地方。
不要背:
onClick={() => {}}
先記這三行:
login
→ function 本身
login()
→ 現在呼叫 function
() => login()
→ 建立另一個 function,之後才呼叫 login()
再放回 React:
onClick
→ 需要一個之後能被呼叫的 function
這樣下次再看到 Arrow Function,就不是靠背語法了。